package com.nightpiter.view;

import java.util.Map;

import android.content.Context;
import android.graphics.Canvas;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ZoomDialog;
import android.widget.ZoomSlider;
import android.widget.ZoomSlider.OnZoomChangedListener;

import com.nightpiter.model.objects.Coordinated;
import com.nightpiter.model.objects.Coordinates;
import com.nightpiter.model.objects.Station;
import com.nightpiter.view.Calculator.Mode;
import com.nightpiter.view.drawer.MetroDrawerWrapper;
import com.nightpiter.view.graphics.Point;
import com.nightpiter.view.graphics.PointF;
import com.nightpiter.view.graphics.Rect;

/**
 * MetroScreenView class. Respond for catching user gestures, notify MVC's
 * MetroView about them, and draw the map retrieved from the drawer to the
 * screen.
 * 
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public class MetroScreenView extends View implements Screen,
		OnZoomChangedListener, MetroDrawerWrapper.RedawingListener {

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	public class PositionCalculator extends Calculator {

		private float scale;

		private float offsetX, offsetY;

		private float pivotX = 0, pivotY = 0;

		/**
		 * private constructor
		 */
		private PositionCalculator() {
			reset();
		}

		private void setPivot(int x, int y) {
			pivotX = x;
			pivotY = y;
		}

		public void centerTo(PointF targetF) {
			offsetX = pivotX - targetF.x;
			offsetY = pivotY - targetF.y;
		}

		@SuppressWarnings("unused")
		private void set(float scale, PointF offset) {
			this.scale = scale;
			this.offsetX = offset.x;
			this.offsetY = offset.y;
		}

		private void reset() {
			this.scale = 1;
			this.offsetX = 0;
			this.offsetY = 0;
		}

		private void scale(float ds) {
			scale *= ds;
			offsetX *= ds;
			offsetY *= ds;
		}

		private void offset(float dx, float dy) {
			offsetX += dx;
			offsetY += dy;
		}

		@Override
		public Mode getMode() {
			return Mode.POSITION;
		}

		@Override
		public void convert(Coordinates coordinates, PointF pointF) {
			pointF.x = (coordinates.x) * scale + offsetX + pivotX;
			pointF.y = (coordinates.y) * scale + offsetY + pivotY;
		}

		@Override
		public void convert(Coordinates coordinates, Point point) {
			point.x = (int) ((coordinates.x) * scale + offsetX + pivotX);
			point.y = (int) ((coordinates.y) * scale + offsetY + pivotY);
		}

		@Override
		public void convert(Coordinated coordinated, PointF pointF) {
			convert(coordinated.getPosition(), pointF);
		}

		@Override
		public void convert(Coordinated coordinated, Point point) {
			convert(coordinated.getPosition(), point);
		}

		@Override
		public void convert(PointF coordF, PointF pointF) {
			pointF.x = (coordF.x) * scale + offsetX + pivotX;
			pointF.y = (coordF.y) * scale + offsetY + pivotY;
		}

		@Override
		public void convert(PointF coordF, Point point) {
			point.x = (int) ((coordF.x) * scale + offsetX + pivotX);
			point.y = (int) ((coordF.x) * scale + offsetX + pivotY);
		}

		@Override
		public void revert(Point point, PointF coordF) {
			coordF.x = (point.x - offsetX - pivotX) / scale;
			coordF.y = (point.y - offsetY - pivotY) / scale;
		}

		@Override
		public void revert(PointF pointF, PointF coordF) {
			coordF.x = (pointF.x - offsetX - pivotX) / scale;
			coordF.y = (pointF.y - offsetY - pivotY) / scale;
		}

		@Override
		public Coordinates getCoordinates(Coordinated coordinated) {
			return coordinated.getPosition();
		}

	}

	/** the subscriber that would be notified about some events */
	private Listener listener;

	/**
	 * The drawer used to analyze model and draw it's representation to the
	 * canvas
	 */
	private MetroDrawerWrapper drawer = MetroDrawerWrapper.getInstance();

	/** zooming slider */
	private ZoomDialog zoomDialog;

	/** view dimensions */
	private int width, height;

	private static final int PAD_SCROLL_STEP = 7;

	private static final int LONG_TOUCH_DURATION = 700;

	private static final int MIN_ZOOM = 1;

	private static final int MAX_ZOOM = 25;

	/** indicate the last action type */
	private boolean wasTouchMove = false;

	/** last touched point coordinates */
	private float lastX = 0, lastY = 0;

	/** station found after last touch */
	private Station[] pendingStationProcessing = null;

	/** current thread handler */
	private Handler handler = new Handler();

	/** enable putting the point onto the center */
	@SuppressWarnings("unused")
	private boolean scrollToPoint = false;

	/** this will show zoomDialog on the screen */
	private Runnable onLongTouchRunnable = new Runnable() {
		public void run() {
			onLongTouchEvent();
		}
	};

	/** point-to-pixel converter */
	private PositionCalculator positionCalculator = new PositionCalculator();

	private boolean forceRedraw;

	/**
	 * @param context
	 */
	public MetroScreenView(Context context) {
		super(context);
		init();
	}

	/**
	 * @param context
	 * @param attrs
	 * @param inflateParams
	 */
	@SuppressWarnings("unchecked")
	public MetroScreenView(Context context, AttributeSet attrs,
			Map inflateParams) {
		super(context, attrs, inflateParams);
		init();
	}

	/**
	 * @param context
	 * @param attrs
	 * @param inflateParams
	 * @param defStyle
	 */

	@SuppressWarnings("unchecked")
	public MetroScreenView(Context context, AttributeSet attrs,
			Map inflateParams, int defStyle) {
		super(context, attrs, inflateParams, defStyle);
		init();
	}

	/**
	 * initialize the drawer with new dimensions
	 */
	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);

		this.width = getWidth();
		this.height = getHeight();

		positionCalculator.setPivot(w / 2, h / 2);

	}

	/**
	 * all class initializations
	 */
	private void init() {
		this.setFocusable(true);

		this.zoomDialog = new ZoomDialog(getContext());
		this.zoomDialog.setParams(MIN_ZOOM, MAX_ZOOM, MIN_ZOOM, this, true);

	}

	@Override
	protected void onDraw(Canvas canvas) {
		canvas.drawARGB(223, 255, 255, 255);
		this.drawer.draw(canvas, positionCalculator, forceRedraw, this);
		forceRedraw = false;
		super.onDraw(canvas);
	}

	public void onImageUpdated() {
		redraw(false);
	}

	public void prepare() {
		drawer.setSize(getWidth(), getHeight());

		this.resetToFit();

		this.zoomDialog.setCurrent(MIN_ZOOM);
		redraw(false);
	}

	private void resetToFit() {
		positionCalculator.reset();
		Rect bounds = drawer.getBounds();
		final int MARGIN = 25;
		float fitScale = Math.min((float) (width - MARGIN) / bounds.width(),
				(float) (height - MARGIN) / bounds.height());
		PointF centerF = new PointF((bounds.getLeft() + bounds.getRight()) / 2,
				(bounds.getTop() + bounds.getBottom()) / 2);
		positionCalculator.scale(fitScale);
		PointF targetF = new PointF();
		positionCalculator.convert(centerF, targetF);
		positionCalculator.centerTo(targetF);
	}

	/**
	 * @param ds
	 */
	private void doScale(float ds) {
		positionCalculator.scale(ds);
		invalidate();
	}

	/**
	 * @param dx
	 * @param dy
	 */
	private void doOffset(int dx, int dy) {
		positionCalculator.offset(dx, dy);
		invalidate();
	}

	/**
	 * this would fired when the screen touched
	 */
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		super.onTouchEvent(event);
		switch (event.getAction()) {

		case MotionEvent.ACTION_DOWN:
			wasTouchMove = false;
			lastX = event.getX();
			lastY = event.getY();
			Point point = new Point((int) lastX, (int) lastY);
			pendingStationProcessing = drawer.searchStation(point,
					positionCalculator);
			handler.removeCallbacks(onLongTouchRunnable);
			handler.postDelayed(onLongTouchRunnable, LONG_TOUCH_DURATION);
			break;

		case MotionEvent.ACTION_MOVE:
			wasTouchMove = true;
			handler.removeCallbacks(onLongTouchRunnable);
			doOffset((int) (event.getX() - lastX), (int) (event.getY() - lastY));
			lastX = event.getX();
			lastY = event.getY();
			break;

		case MotionEvent.ACTION_UP:
			handler.removeCallbacks(onLongTouchRunnable);
			if (!wasTouchMove) {
				if (pendingStationProcessing != null) {
					listener.onStationRetrieved(pendingStationProcessing);
					scrollToPoint = false;
					pendingStationProcessing = null;
				}
			}
			break;
		}
		return true;

	}

	/**
	 * this would fired when the screen touched for a long time
	 */
	public void onLongTouchEvent() {
		if (pendingStationProcessing.length != 0) {
			listener.onStationRetrieved(pendingStationProcessing);
			scrollToPoint = true;
		} else {
			zoomDialog.setPosition(this, (int) lastX, (int) lastY + 22);
			zoomDialog.show();
		}
		pendingStationProcessing = null;
	}

	public void showZoom() {
		zoomDialog.setPosition(this, 60, 400);
		zoomDialog.show();
	}

	public void onZoomChanged(ZoomSlider zoomSlider, int oldZoom, int newZoom) {
		this.doScale((float) (newZoom + 1) / (float) (oldZoom + 1));
	}

	public void onZoomCompleted() {
		invalidate();
	}

	public void onZoomIn(ZoomSlider zoomSlider, int oldZoom, int newZoom) {
		this.doScale((float) (newZoom + 1) / (float) (oldZoom + 1));
	}

	public void onZoomOut(ZoomSlider zoomSlider, int oldZoom, int newZoom) {
		this.doScale((float) (newZoom + 1) / (float) (oldZoom + 1));
	}

	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		switch (keyCode) {
		case KeyEvent.KEYCODE_DPAD_UP:
			this.doOffset(0, PAD_SCROLL_STEP);
			break;
		case KeyEvent.KEYCODE_DPAD_DOWN:
			this.doOffset(0, -PAD_SCROLL_STEP);
			break;

		case KeyEvent.KEYCODE_DPAD_LEFT:
			this.doOffset(PAD_SCROLL_STEP, 0);
			break;

		case KeyEvent.KEYCODE_DPAD_RIGHT:
			this.doOffset(-PAD_SCROLL_STEP, 0);
			break;

		case KeyEvent.KEYCODE_DPAD_CENTER:
			this.resetToFit();
			invalidate();
			break;

		}
		return true;
	}

	public void redraw(boolean force) {
		this.forceRedraw = force;
		invalidate();
	}

	public View getView() {
		return this;
	}

	public Mode getMode() {
		return positionCalculator.getMode();
	}

	/**
	 * subscribe to notifications
	 * 
	 * @param listener
	 *            the listener
	 */
	public void setListener(Listener listener) {
		if (listener != null) {
			this.listener = listener;
		}
	}

}
